﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using EBookStore.Users;
using EBookStore.Orders;

namespace EBookStore.Management
{
    /// <summary>
    /// In chanrge to manage orders 
    /// send/recieve to publishers.
    /// send/recieve to customers.
    /// </summary>    
    public class OrdersManager
    {
        /// <summary>
        /// Amount of days in whitch book can be returned to store
        /// after it has been delivered.
        /// </summary>
        private static readonly int  BOOK_CAN_BE_RETURNED_IN_DAYS = 10;

        List<Orders.Order> _orders;

        #region Properties

        /// <summary>
        /// List of all orders
        /// </summary>
        public List<Order> OrdersList { get { return _orders; } }

        #endregion


        #region Singleton

        private static OrdersManager _instance = null;
        private static object _syncObj = new object();

        /// <summary>
        /// Catalog instance
        /// </summary>
        public static OrdersManager Instance
        {
            get
            {
                if (_instance == null)
                {
                    lock (_syncObj)
                    {
                        if (_instance == null)
                        {
                            _instance = new OrdersManager();
                        }
                    }
                }
                return _instance;
            }
        }


        private OrdersManager()
        {
            _orders = new List<Order>();
        }


        #endregion


        #region Get Orders By Filter

        /// <summary>
        /// return orders assosiated to user.
        /// </summary>
        /// <param name="user"></param>
        /// <returns></returns>
        public IEnumerable<Order> GetOrders(User user)
        {
            return getOrdersFromList(this.OrdersList, user);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="status"></param>
        /// <returns></returns>
        private IEnumerable<Order> GetOrders(eOrderStatus status)
        {
            return getOrdersFromList(this.OrdersList, status);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="user"></param>
        /// <param name="status"></param>
        /// <returns></returns>
        private IEnumerable<Order> GetOrders(User user,eOrderStatus status)
        {
            return getOrdersFromList(this.OrdersList,user, status);
        }
        #endregion



        #region Order's Actions

        /// <summary>
        /// 
        /// </summary>
        /// <param name="u"></param>
        /// <param name="eov"></param>
        /// <param name="p"></param>
        /// <param name="sbos"></param>
        /// <returns></returns>        
        [AOP.NotNullOrConcreteObject(Index=0, BaseType= typeof(Users.User), ConcreteType=typeof(Users.Customer), Message="User is null")]
        [AOP.NotNullOrConcreteObject(Index = 2, BaseType = typeof(Payments.Payment) , Message="Payment can't be null")]
        [AOP.NotNullOrConcreteObject(Index = 3, BaseType = typeof(SingleBookOrder), Message="Books order array is null")]
        [AOP.Trace]
        [AOP.TraceException]
        public Order CreateOrder(User u, eOrderVia eov, Payments.Payment p, SingleBookOrder[] sbos)
        {
            #region Validate Data
          
            if (p.Payments > 1)
            {
                if (p.Sum != p.FirstPayment + (p.OtherPayments * p.Payments))
                {
                    throw new Exception("Total sum payment is not the same as sum of payments");
                }
            }          
            foreach (SingleBookOrder bo in sbos)
            {
                if (bo == null)
                {
                    throw new Exception("Book order is null");
                }
                if (bo.Amount < 1)
                {
                    throw new Exception("invalid book amount");
                }
                if (bo.Book == null)
                {
                    throw new Exception("book is null object");
                }
            } 

            if (!p.Charge())
            {
                throw new Exception("Failed to charge order");
            }


            #endregion

            Order o = new Order(u, eov, p);
            o.AddBooks(sbos);
            o.UpdateOrderStatus(eOrderStatus.Charged);

            o.OnStatusChange += new EventHandler<OrderStatusEventArgs>(Order_OnStatusChange);

            OrdersList.Add(o);                           
            return o;
        }

        /// <summary>
        /// Hadles order status change events.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Order_OnStatusChange(object sender, Orders.OrderStatusEventArgs e)
        {
            switch (e.Order.Status)
            {
                case eOrderStatus.Canceled:
                    {
                        InternalMessageAboutOrderCancelation(e.Order);
                        break;
                    }
            }
            
        }


        /// <summary>
        /// 
        /// </summary>
        /// <param name="u"></param>
        /// <param name="OrderID"></param>
        /// <returns></returns>
        [AOP.NotNullOrConcreteObject(Index = 0, BaseType = typeof(Users.User), ConcreteType=typeof(Users.Customer), Message = "User is null")]
        [AOP.NotNullOrConcreteObject(Index = 1, BaseType = typeof(string), Message = "order id can't be null")]         
        [AOP.Trace]
        [AOP.TraceException]
        public bool CancelOrder(User u, string OrderID)
        {           
            Orders.Order[] tmpOrders = (Orders.Order[])GetOrders(u);
            Orders.Order userOrder = (from el in tmpOrders where el.ID.Equals(OrderID) select el).FirstOrDefault();

            if (userOrder == null )
                throw new Exception("Order not found");
            
            switch (userOrder.Status)
            {
                case eOrderStatus.Created:
                    {
                        userOrder.UpdateOrderStatus(eOrderStatus.Closed);
                        break;
                    }
                case eOrderStatus.Charged:
                    {
                        if (!userOrder.Payment.Refund())
                        {
                            throw new Exception("Cannot refund");
                        }
                        userOrder.UpdateOrderStatus(eOrderStatus.Closed);
                        break;

                    }
                case eOrderStatus.AllBooksOrderedFromPublisher:
                    {
                        if (!userOrder.Payment.Refund())
                        {
                            throw new Exception("Cannot refund");
                        }
                        userOrder.UpdateOrderStatus(eOrderStatus.Closed);
                        break;

                    }
                case eOrderStatus.AllBooksRecievedFromPublisher:
                    {
                        
                            if (!userOrder.Payment.Refund())
                            {
                                throw new Exception("Cannot refund");
                            }
                          
                            foreach (SingleBookOrder sb in userOrder.BookList)
                            {
                                if (!ReturnBook(u, OrderID, sb.Book, sb.Amount))
                                {
                                    throw new Exception("Cannot return book");
                                }
                            }
                            
                            userOrder.UpdateOrderStatus(eOrderStatus.Closed);
                           
                            break;

                        
                    }

                case eOrderStatus.AllBooksSentToCustomer:
                    {
                        throw new Exception("Cannot Cancel, book is already sent/delivered to the customer");
                    }
                case eOrderStatus.Closed:
                    {
                        throw new Exception("Order already Closed");
                    }
                default:
                    {
                        throw new Exception("Unkown status:'" + userOrder.Status + "' can't cancel");
                    }
            }

            return true;
        }
        
        /// <summary>
        /// 
        /// </summary>
        /// <param name="u"></param>
        /// <param name="OrderID"></param>
        /// <param name="book"></param>
        /// <param name="amount"></param>
        [AOP.NotNullOrConcreteObject(Index = 0, BaseType = typeof(Users.User), ConcreteType=typeof(Users.Customer), Message = "User is null")]
        [AOP.NotNullOrConcreteObject(Index = 1, BaseType = typeof(string), Message = "Order id can't be null")]
        [AOP.NotNullOrConcreteObject(Index = 2, BaseType = typeof(Books.Book), Message = "Book can't be null")]  
        [AOP.IntMinValue(Index =3,MinValue=1)]
        [AOP.Trace]
        [AOP.TraceException]
        public bool ReturnBook(User u, string OrderID, Books.Book book, int amount)
        {                                   
            Orders.Order []userOrder = (Orders.Order[])GetOrders(u);
            if (userOrder == null || userOrder.Length == 0)
                throw new Exception("Order not found");
            Order currOrder = userOrder[0];
            if(PublisherManager.Instance.ReturnBook(book, amount)) 
            {
                SingleBookOrder currSbo = (from sbo in currOrder.BookList where sbo.Book.ISBN == book.ISBN select sbo).FirstOrDefault();
                currSbo.Amount -= amount;
                if (currSbo.Amount <= 0)
                    currSbo.UpdateBookStatus(eBookOrderStatus.Closed);
            }
            return true;
            
        }

        
        #endregion




        #region Automated Order's actions

        /// <summary>
        /// Search in all orders for unOrdered books
        /// and request them from publishers.
        /// </summary>
        public void OrderBooksFromPublishers()
        {
            Orders.Order[] tmpOrders = (Orders.Order[])getOrdersFromList(OrdersList, eOrderStatus.Charged);

            foreach (Orders.Order order in tmpOrders)
            {
                // send ordered books to publishers
                foreach (Orders.SingleBookOrder single in order.BookList)
                {
                    if (single.Status == eBookOrderStatus.OrderedByCustomer && (single.SuppliedToCustomer < single.Amount))
                    {
                        Management.PublisherManager.Instance.OrderBook(single.Book, single.Amount - single.SuppliedToCustomer);
                        single.UpdateBookStatus(eBookOrderStatus.OrderedFromPublisher);
                    }
                }
                // if all books in order already requested from publisher update order status.
                bool allBooksOrdered = true;
                foreach (Orders.SingleBookOrder single in order.BookList)
                {
                    if ((int)single.Status < (int)eBookOrderStatus.OrderedFromPublisher)
                    {
                        allBooksOrdered = false;
                        break;
                    }
                }
                if (allBooksOrdered)
                {
                    order.UpdateOrderStatus(eOrderStatus.AllBooksOrderedFromPublisher);
                }
            }
        }


        /// <summary>
        /// Recieves books from publishers
        /// insert them into orders        
        /// </summary>
        public void RecieveBooksFromPublishers()
        {
            // recieve books from publishers.
            List<Orders.BookAndAmount> ba = Management.PublisherManager.Instance.BooksArrivedFromPublisher;


            Orders.Order[] tmpOrders = (Orders.Order[])getOrdersFromList(OrdersList, eOrderStatus.AllBooksOrderedFromPublisher);
            foreach (Orders.Order order in tmpOrders)
            {
                foreach (Orders.BookAndAmount bookAmount in ba)
                {
                    foreach (Orders.SingleBookOrder singleBook in order.BookList)
                    {
                        // check this is the neede book and it wasn't already fully recieved from publisher
                        if (singleBook.Book.ISBN.Equals(bookAmount.Book.ISBN) && ((int)singleBook.Status < (int)eBookOrderStatus.RecievedFromPublisher))
                        {
                            int amount = bookAmount.Amount;
                            int amountToDeliver = 0;
                            int booksLeftToDeliver = singleBook.UpdateBooksAmountSuppliedByPublisher(ref amount, out amountToDeliver);
                            bookAmount.Amount = amount;
                            if (booksLeftToDeliver <= 0)
                            {
                                // update single book status
                                singleBook.UpdateBookStatus(eBookOrderStatus.RecievedFromPublisher); // all amount of books recieved 
                            }
                            if (amountToDeliver > 0)
                            {
                                SendFullOrPartialOrdersToCustomers(order, singleBook.Book, amountToDeliver);
                            }
                        }
                    }
                }                
            }

        }



        /// <summary>
        /// Sends fully or partialy created orders to customers.
        /// </summary>
        public void SendFullOrPartialOrdersToCustomers(Order order, Books.Book book, int amount)
        {
             // sending the books ....

            foreach (SingleBookOrder so in order.BookList)
            {
                if (so.Book.ISBN.Equals(book.ISBN) && so.Amount == so.SuppliedToCustomer)
                {
                    so.UpdateBookStatus(eBookOrderStatus.SentToCustomer);
                }
            }

           
        }


        /// <summary>
        /// Checks whats orders can be closed completely.
        /// </summary>
        public void CloseOrders()
        {

            var orders = from el in OrdersList where el.Status != eOrderStatus.Closed select el;

            foreach (Orders.Order order in orders)
            {

                bool canCloseOrder = true;

                               
                foreach (SingleBookOrder so in order.BookList)
                {
                    // if book NOT delevered/canceled/returned change order status
                    if ((int)so.Status < (int)eBookOrderStatus.CanceledByCustomer)
                    {
                        canCloseOrder = false;
                        break;
                    }
                    else
                    {
                        // if sended and not returned in 10 days
                        if (so.Status == eBookOrderStatus.SentToCustomer)
                        {
                            TimeSpan span = DateTime.Now.Subtract(so.HistoryStatus.Last().Date);
                            if (span.Days >= BOOK_CAN_BE_RETURNED_IN_DAYS)
                            {
                                so.UpdateBookStatus(eBookOrderStatus.Closed);
                            }
                            else
                            {
                                canCloseOrder = false;
                                break;
                            }
                        }                        
                    }                    
                }
                if (canCloseOrder)
                    order.UpdateOrderStatus(eOrderStatus.Closed);
            }
        }

        
        #endregion
        

 

        #region Private


        /// <summary>
        /// When order is canceled, specified users will be acknowledged about it.
        /// </summary>
        private void InternalMessageAboutOrderCancelation(Order order)
        {
            Users.User []users = (Users.User[])UsersManager.Instance.GetUsersForOrderCancelationAcknowledge();
            foreach (Users.User u in users)
            {
                u.SendMessage(string.Format("Order num:{0} has been canceled", order.ID));
            }
        }


        /// <summary>
        /// Get all user's orders.
        /// </summary>
        /// <param name="user"></param>
        /// <returns></returns>
        private IEnumerable<Order> getOrdersFromList(IEnumerable<Orders.Order> orders, User user)
        {
            return (from el in orders 
                    where el.User.ID.Equals(user.ID) 
                    orderby el.HistoryStatus[0].Date ascending 
                    select el).ToArray<Order>();
        }
        
        /// <summary>
        /// gets orders by status 
        /// </summary>
        /// <param name="orders"></param>
        /// <param name="orderStatus"></param>
        /// <returns></returns>
        private IEnumerable<Order> getOrdersFromList(IEnumerable<Orders.Order> orders,Orders.eOrderStatus orderStatus)
        {
            return (from el in orders 
                    where el.Status == orderStatus
                    orderby el.HistoryStatus[0].Date ascending
                    select el).ToArray<Order>();
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="orders"></param>
        /// <param name="user"></param>
        /// <param name="orderStatus"></param>
        /// <returns></returns>
        private IEnumerable<Order> getOrdersFromList(IEnumerable<Orders.Order> orders, User user, Orders.eOrderStatus orderStatus)
        {
            IEnumerable<Orders.Order> tmp = getOrdersFromList(orders, user);
            return getOrdersFromList(tmp, orderStatus);
        }

        #endregion

        



        /// <summary>
        /// 
        /// </summary>
        /// <param name="u"></param>
        /// <param name="orderID"></param>
        /// <returns></returns>
        public IEnumerable<eOrderStatus> CheckOrderStatus(User u, int orderID)
        {
            return (from o in OrdersList 
                    where o.ID.Equals(orderID) && (u.GetType() == typeof(Clerk) ? true : u.ID == o.User.ID)
                    orderby o.HistoryStatus[0].Date ascending
                    select o.Status);
        }
       
    }
}
